Appearance
Gson 反序列化会将 int 转换为 double 类型的问题与解决方案
使用 Gson 反序列化,在可以确定 JSON 内容(Key 数量不变)的情况下,我们通常会建立与之对应的 Java POJO,Gson 会自动帮我们映射到对应的类中;而在 JSON 内容不确定(Key 数量未知)的情况下,映射到一个 Map
中是比较合适的。
在 Maven 项目中添加 Gson 依赖:
xml
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
Gson 反序列化映射到 POJO
比如说有这样一个 JSON:
json
{
"id":6,
"name":"小明",
"score":88.5
}
建立对应的 POJO:
java
package study.helloworld.gson;
public class Student {
private int id;
private String name;
private double score;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", score=" + score +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
}
使用 Gson 反序列化:
java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Gson gson = new Gson();
Student student = gson.fromJson(json, Student.class);
System.out.println(student); // Student{id=6, name='小明', score=88.5}
Gson 反序列化映射到 Map
如果将上面的 JSON 映射到 Map
呢?
java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Gson gson = new Gson();
Map map = gson.fromJson(json, Map.class);
最好返回一个带泛型的 Map
。JSON 的 Key 都是字符串,Map
中的 Key 可以定义成 String
。但是 JSON 里的 Value 的类型是不同的,有整数、字符串和小数,所以 Map
中的 Value 只能定义成 Object
:
java
Map<String, Object> map = gson.fromJson(json, new TypeToken<Map<String, Object>>() {}.getType());
结果都是一样的:
java
System.out.println(map); // {id=6.0, name=小明, score=88.5}
问题来啦!“id” 是一个整数,转换后却变成了小数。虽然在语义上“6 和 6.0 相等”,但是在逻辑上,“id”从 int
类型变成了 double
类型,这是不被允许的。
Gson 反序列化的过程
反序列化时,根据要序列化的 Type
, 得到 TypeToken
,再使用图中标注的 getAdapter
方法获取一个 TypeAdapter
。
(图片来源:自己截的)
TypeAdapter
是怎么生成的呢?Gson 内置了许多不同的 TypeAdapterFactory
,在实例化 Gson
对象的时候,将其全部放到了一个 List
中。
(图片来源:自己截的)
TypeAdapter
是一个抽象类,里面的 write
和 read
抽象方法分别表示“序列化”和“反序列化”,需要由子类实现。下图是子类实现(部分):
(图片来源:自己截的)
TypeAdapter
是怎么获取的呢?在 getAdapter
方法中,根据不同的 Type
,调用不同的 TypeAdapterFactory
,从而得到对应的 TypeAdapter
。
(图片来源:自己截的)
得到具体的 TypeAdapter
后,开始读取(read)具体的值。由于我们使用的 Type
是 Map<String, Object>
,在匹配到 Object
类型的时候,会返回一个 ObjectTypeAdapter
。
ObjectTypeAdapter
是如何 read
值的呢?首先,会 peek
这个值,然后确定这个值具体属于什么类型(数组、字符串、数字等),从而返回对应的类型。
(图片来源:自己截的)
int 为什么变成了 double
注意上图中的标注,如果 Object
属于一个 NUMBER
类型,那么会返回一个 double
类型的值。“6”是一个数字,自然而然,“6”会变成“6.0”。
为什么会这样呢?
查阅不少资料,普遍有两种说法:
- 在 JSON 规范中,只有数字类型,而没有整数类型和小数类型。显然,使用精度最高的
double
是最合适的方式。 - 这是一个 Bug,Gson 开发者可能还没有找到更合适的处理方式来解决这个问题。
解决方案
这里推荐几种解决方案供大家选择。首先说明,这些方案都不完美,各有优缺点。
编写自己的 ObjectTypeAdapter
既然官方的 ObjectTypeAdapter
达不到我们想要的效果,那么就自己编写一个。
java
package study.helloworld.gson;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 自己的 ObjectTypeAdapter,只能处理反序列化,可以解决 int 变成 double 的问题
*/
public final class MyObjectTypeAdapter extends TypeAdapter<Object> {
/**
* 不支持序列化,使用官方库进行序列化
* @param jsonWriter
* @param o
*/
@Override
public void write(JsonWriter jsonWriter, Object o) {
throw new UnsupportedOperationException();
}
@Override
public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch (token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList();
in.beginArray();
while (in.hasNext()) {
list.add(this.read(in));
}
in.endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap();
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), this.read(in));
}
in.endObject();
return map;
case STRING:
return in.nextString();
case NUMBER:
// 优点——可以解决 `int` 到 `int`,且不丢失数据类型(`instanceof = Number`)
// 缺点——无法解决 `double` 中的小数部分恰好是“0”的情况(比如“6.0”,反序列化后反而变成了“6”)
double aDouble = in.nextDouble();
long aLong = (long)aDouble;
if (aDouble == aLong) {
return aLong;
}
return aDouble;
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new IllegalStateException();
}
}
}
深色部分,也就是 switch
中的 NUMBER
代码块中,改写了原来的代码。
- 优点——可以解决
int
到int
,且不丢失数据类型(instanceof = Number
) - 缺点——无法解决
double
中的小数部分恰好是“0”的情况(比如“6.0”,反序列化后反而变成了“6”)
还有一种方案是直接返回一个字符串类型的数字:
java
// 优点——可以完美解决反序列化结果不一致的问题
// 缺点——数据类型都变成了字符串(`instanceof = String`)
double aDouble = in.nextDouble();
String number = String.valueOf(aDouble);
return number;
- 优点——可以完美解决反序列化结果不一致的问题
- 缺点——数据类型都变成了字符串(
instanceof = String
)
这两种方案的使用方法都是一样的,通过 GsonBuilder
注册一个 TypeAdapter
来创建一个 Gson
对象:
java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(type, new MyObjectTypeAdapter()).create();
Map<String, Object> map = gson.fromJson(json, type);
System.out.println(map); // {id=6, name=小明, score=88.5}
编写自己的 JsonDeserializer
在 Gson 2.0 版本之前,序列化和反序列化是通过 JsonSerializer
和 JsonDeserializer
来完成的。所以我们也可以定义自己的 JsonDeserializer
。
java
package study.helloworld.gson;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 自己的 JsonDeserializer,可以完美解决 int 变成 double 的问题
* 但是数据类型都变成了 Object(`instanceof = Object`)
* 更诡异的是字符串反序列化后带有双引号
*/
public class MyJsonDeserializer implements JsonDeserializer<Map<String, Object>> {
@Override
public Map<String, Object> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
HashMap<String, Object> hashMap = new LinkedHashMap<>();
JsonObject jsonObject = jsonElement.getAsJsonObject();
Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
for (Map.Entry<String, JsonElement> entry : entrySet) {
hashMap.put(entry.getKey(), entry.getValue());
}
return hashMap;
}
}
同样的,也是通过 GsonBuilder
构建一个 Gson
对象:
java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(type, new MyJsonDeserializer()).create();
Map<String, Object> map = gson.fromJson(json, type);
System.out.println(map); // {id=6, name="小明", score=88.5}
- 优点——可以完美解决反序列化结果不一致的问题
- 缺点——数据类型都变成了 Object(
instanceof = Object
)
这个方案更诡异的是字符串反序列化后会带有双引号(注意打印的输出内容)。